package commons;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
/**
 * 
 * PDU Implementation
 * puts into proper format and extracts from it
 *
 */
public class Message {
	
	public static final int	CONNECT 	= 1;
	public static final int BAD_AUTH 	= 2;
	public static final int CONNECT_OK 	= 3;
	public static final int GAME_LIST	= 5; 
	public static final int JOIN_GAME 	= 6;
	public static final int	JOIN_FAIL 	= 7;
	public static final int	JOIN_ACK 	= 8;
	public static final int	GAME_UPDATE = 9;
	public static final int	PING 		= 10; 
	public static final int	PONG 		= 11;
	public static final int	GAME_OVER 	= 12;
	public static final int	DISCONNECT 	= 13;
	public static final int	LIST_REQUEST = 14;
	
	// define size of each fields
	private static final int SERVER_NAME_SIZE = 12;
	private static final int PAYLOAD_LENGTH_SIZE = 4;
	private static final int GAME_ID_SIZE = 2;
	private static final int GAME_NAME_SIZE = 12;
	
	private static final int TIME_STAMP_SIZE = 4;
	private static final int MESSAGE_CODE_SIZE = 1;
	private static final int VERSION_SIZE = 1;
	private static final int USERNAME_SIZE = 12;
	private static final int PASSWORD_SIZE = 12;
	
	private static int time = 0;
	
	public static final int MAX_PACKET_LENGTH = 1024;
	
	
	//header
	private int messageCode;
	private int version = 1; //version of the game protocol
	private String serverName;
	private short gameId; //game identifier
	String username;
	
	String password;
	//payload
	private int payloadLength;
	//private char[] payload; //actual payload, size depends on message code
	private byte[] payload; //actual payload, size depends on message code
	
	private int timeStamp;
	private HashMap<Integer,String> gameList;
	private boolean read;

    /**
     * default constructor
     */
	
	public Message() {
		// TODO Auto-generated constructor stub
		this.read = true;
	}

	public Message( 
		String serverName, 
		Integer gameId,  
		int messageCode,
		char[] payload) 
	{
		
		this.serverName = serverName;
		this.gameId = gameId.shortValue();
		this.messageCode = messageCode;
		
		this.payloadLength = payload.length;
		//this.payload = new char[this.payloadLength];
		this.payload = new byte[this.payloadLength];
		
		
		// Copy and convert bytes to chars

		//System.arraycopy(payload, 0, this.payload, 0, payload.length);
		System.arraycopy(payload, 0, this.payload, 0, payload.length);


	}
	
	public Message( 
			String serverName, 
			Integer gameId,  
			int messageCode,
			byte[] payload) 
		{
			
			this.serverName = serverName;
			this.gameId = gameId.shortValue();
			this.messageCode = messageCode;
			
			this.payloadLength = payload.length;
			this.payload = new byte[this.payloadLength];
			
			
			// Copy and convert bytes to chars

			System.arraycopy(payload, 0, this.payload, 0, payload.length);

		}

	/**
	 * Message to send login information
	 * @param serverName
	 * @param gameId
	 * @param messageCode
	 * @param username
	 * @param password
	 */
	public Message( 
			String serverName, 
			Integer gameId,  
			int messageCode, 
			String username,
			String password)
	{
		this.serverName = serverName;
		this.gameId = gameId.shortValue();
		this.messageCode = messageCode;
		
		// Establish login payload
		this.payloadLength = USERNAME_SIZE + PASSWORD_SIZE;
		this.payload = new byte[payloadLength];
		
		System.arraycopy(username.getBytes(), 0, this.payload, 0, username.length());
		System.arraycopy(password.getBytes(), 0, this.payload, USERNAME_SIZE, password.length());
		
	}
	

	

	public Message(String serverName, int messageCode) {
		this.serverName = serverName;
		this.gameId = (short)0;
		this.messageCode = messageCode;
		
		// Establish payload
		this.payloadLength = 0;
	}

	
	/**
	 * Constructor for messages that takes only username as payload
	 * @param serverName
	 * @param gameId
	 * @param messageCode
	 * @param username
	 */
	public Message(String serverName, 
			Integer gameId,  
			int messageCode, 
			String username) {
		
		this.serverName = serverName;
		this.gameId = gameId.shortValue();
		this.messageCode = messageCode;
		
		// Establish payload
		this.payloadLength = USERNAME_SIZE ;
		//this.payload = new char[payloadLength];
		this.payload = new byte[payloadLength];
		
		//System.arraycopy(username.toCharArray(), 0, this.payload, 0, username.length());
		System.arraycopy(username.getBytes(), 0, this.payload, 0, username.length());

	}

   /**
    * Message for sending game list	
    * @param servername
    * @param gameList
    */
	public Message(String servername,
			       HashMap<Integer, World> gameList) {
		
		
		this.serverName = servername;
		this.gameId = -1;
		this.messageCode = GAME_LIST;
		this.username = "";
		
		int numberOfGames = gameList.size();
		this.payloadLength = USERNAME_SIZE+(GAME_ID_SIZE+GAME_NAME_SIZE)*numberOfGames;
		this.payload = new byte[payloadLength];
		
		System.arraycopy(username.getBytes(), 0, this.payload, 0, username.length());
		
		int offset = USERNAME_SIZE;
		
		for(Iterator<Integer> keys = gameList.keySet().iterator(); keys.hasNext();) {
			Integer key = keys.next();
			util.Lib.bytesFromInt(this.payload, offset, key);
			offset+=GAME_ID_SIZE;
			String gameName = gameList.get(key).getName();
			System.arraycopy(gameName.getBytes(), 0, this.payload, offset, gameName.length());
			
			offset+=GAME_NAME_SIZE;
		}
		
	}

	
	/**
	 *  Takes fields of object and returns as a char array
	 */
	public byte[] toBytes(){
		byte[] packet = new byte[
		                         // Header
		                         VERSION_SIZE + SERVER_NAME_SIZE + GAME_ID_SIZE + TIME_STAMP_SIZE +
		                         MESSAGE_CODE_SIZE + 
		                         PAYLOAD_LENGTH_SIZE +
		                         this.payloadLength];
		int offset = 0;
		// Version
		util.Lib.bytesFromInt(packet, offset, this.version);
		offset += VERSION_SIZE;
		
		// Server name
		System.arraycopy(this.serverName.getBytes(), 0, packet, offset, this.serverName.length());
		
		offset += SERVER_NAME_SIZE;
		
		// Game id
		util.Lib.bytesFromShort(packet, offset, this.gameId);
		offset += GAME_ID_SIZE;
		
		// Timestamp 
		util.Lib.bytesFromInt(packet, offset, time++);
		offset += TIME_STAMP_SIZE;
		
		// Message cade
		util.Lib.bytesFromInt(packet, offset, this.messageCode);
		offset += MESSAGE_CODE_SIZE;
		
		// Payload length
		util.Lib.bytesFromInt(packet, offset, this.payloadLength);
		offset += PAYLOAD_LENGTH_SIZE;
		
		// Payload
		if(payloadLength > 0 && this.payload!=null) {
			System.arraycopy(this.payload, 0, packet, offset, this.payloadLength);
		} 
		else
		{
			System.err.println("Sending 0 byte payload");
		}

		// TODO Copy header as footer, or update document
		return packet;
	}
	
	public void read() {
		this.read = true;
	}
	
	public boolean isRead() {
		return this.read;
	}

	/**
	 * Extracting received messages
	 * @param in
	 * @throws IOException
	 */

	public Message(DataInputStream in) throws IOException
	{		
		System.err.println("Extracting messages...");
		byte[] input = new byte[MAX_PACKET_LENGTH];
		int numberOfBytesRead = in.read(input);
		if(numberOfBytesRead < 0)
			throw new IOException("Stream failed");
		
		this.read = false;
		int offset = 0;
		
		// Version
		this.version = util.Lib.bytesToInt(input, offset, VERSION_SIZE);
		offset += VERSION_SIZE;
		
		// Server name
		this.serverName = util.Lib.bytesToString(input, offset, SERVER_NAME_SIZE);
		offset += SERVER_NAME_SIZE;
		
		// Game id
		this.gameId = util.Lib.bytesToShort(input, offset);
		offset += GAME_ID_SIZE;
		
		
		// Time stamp 
		this.timeStamp = util.Lib.bytesToInt(input, offset, TIME_STAMP_SIZE);
		offset += TIME_STAMP_SIZE;
		
		// Message code
		this.messageCode = util.Lib.bytesToInt(input, offset, MESSAGE_CODE_SIZE);
		offset += MESSAGE_CODE_SIZE;
		
		// Payload length
		this.payloadLength = util.Lib.bytesToInt(input, offset, PAYLOAD_LENGTH_SIZE);
		if(payloadLength > 0) 
		{
			offset += PAYLOAD_LENGTH_SIZE;
			//this.payload = new char[this.payloadLength];
			this.payload = new byte[this.payloadLength];
			
			System.arraycopy(input, offset, this.payload, 0, this.payloadLength);
			
			// payload depends on message code 
			switch(this.messageCode) {
			case CONNECT: 
				this.username = util.Lib.bytesToString(input, offset, USERNAME_SIZE);
				offset += USERNAME_SIZE;
				
				this.password = util.Lib.bytesToString(input, offset, PASSWORD_SIZE);
				offset += PASSWORD_SIZE;
				break;
				
			case CONNECT_OK:
			case DISCONNECT:
			case PING:
			case PONG:
			case JOIN_ACK:
			case JOIN_FAIL:
			case LIST_REQUEST:
				this.username = util.Lib.bytesToString(input, offset, USERNAME_SIZE);
				break;
			
			case GAME_LIST:
				int bytesLeft= payloadLength;
				this.username = util.Lib.bytesToString(input, offset, USERNAME_SIZE);
				offset += USERNAME_SIZE;
				bytesLeft-=USERNAME_SIZE;
				
				gameList = new HashMap<Integer,String>();
				// Note that the last byte in the stream is some flush byte
				// took me a while to understand that (gao).
				while(bytesLeft>0) {
					
					int id = util.Lib.bytesToInt(input, offset, GAME_ID_SIZE);
					offset += GAME_ID_SIZE;
					bytesLeft-=GAME_ID_SIZE;
					
					//char[] name = new char[GAME_NAME_SIZE];
					String name = util.Lib.bytesToString(input, offset,GAME_NAME_SIZE);
					//System.out.println("name->"+name);
					offset += GAME_NAME_SIZE;
					bytesLeft-=GAME_NAME_SIZE;
					//System.out.println("offset->"+offset);
					
					gameList.put(id, name);
				}
				break;
			case JOIN_GAME:
				this.username = util.Lib.bytesToString(input, offset, USERNAME_SIZE);
				offset += USERNAME_SIZE;
				break;
			case GAME_UPDATE:
				break;
			}
		}
		
	}
	
	public HashMap<Integer, String> getGameList() {
		return this.gameList;
	}
	
	// Accessor methods used to retrieve information about the received packet
	public String getUsername() {
		return username;
	}


	public String getPassword() {
		return password;
	}


	public short getGameId() {
		return gameId;
	}


	public int getMessageCode() {
		return messageCode;
	}


	public byte[] getPayload() {
		return payload;
	}


	public int getPayloadLength() {
		return payloadLength;
	}


	public String getServerName() {
		return serverName;
	}


	public int getVersion() {
		return version;
	}
	
	
	public void send(DataOutputStream out) {
		System.err.println("SENDING MESSAGE: " + getMessageDescription());
		try {
			out.write(this.toBytes());
			out.flush();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
	}
	
	
	
	public String getMessageDescription() {
		switch(this.messageCode) {
		case CONNECT:
			return "[connect request]";
		case BAD_AUTH:
			return "[bad authentication]";
		case CONNECT_OK:
			return "[connection ok]";
		case GAME_LIST:
			return "[game list]";
		case JOIN_GAME:
			return "[join game]";
		case JOIN_FAIL:
			return "[join failed]";
		case JOIN_ACK:
			return "[join game ok]";
		case GAME_UPDATE:
			return "[game update]";
		case PING:
			return "[ping]";
		case PONG:
			return "[pong]";
		case GAME_OVER:
			return "[game over]";
		case DISCONNECT:
			return "[disconnect]";
		case LIST_REQUEST:
			return "[game list request]";
		default:
			return "[error!!]";
		}
	}

	public int getTimeStamp() {
		return timeStamp;
	}
	

}

